#!/usr/bin/env python
# Copyright 2012 Nick Foster
# 
# This file is part of gr-air-modes
# 
# gr-air-modes is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
# 
# gr-air-modes is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with gr-air-modes; see the file COPYING.  If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
# 

import os, sys, time, threading, datetime, math, csv, tempfile, ConfigParser
from optparse import OptionParser
from PyQt4 import QtCore,QtGui,QtWebKit
from PyQt4.Qwt5 import Qwt
from gnuradio import gr, eng_notation
from gnuradio.eng_option import eng_option
from gnuradio.gr.pubsub import pubsub
import air_modes
from air_modes.exceptions import *
from air_modes.modes_rx_ui import Ui_MainWindow
from air_modes.gui_model import *
from air_modes.az_map import *
import sqlite3
import zmq

class mainwindow(QtGui.QMainWindow):
    live_data_changed_signal = QtCore.pyqtSignal(QtCore.QString, name='liveDataChanged')
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        #set defaults
        #add file, RTL, UHD sources
        self.ui.combo_source.addItems(["UHD", "Osmocom", "File/UDP"])
        self.ui.combo_source.setCurrentIndex(0)

        #populate antenna, rate combo boxes based on source
        self.populate_source_options()

        defaults = self.get_defaults()

        #should round to actual achieved gain
        self.ui.line_gain.insert(defaults["gain"])

        #default to 5dB
        self.ui.line_threshold.insert(defaults["threshold"])

        if defaults["pmf"] is not None:
            self.ui.check_pmf.setChecked(bool(defaults["pmf"]))
        if defaults["dcblock"] is not None:
            self.ui.check_dcblock.setChecked(bool(defaults["dcblock"]))
        if defaults["samplerate"] is not None:
            if defaults["samplerate"] in self.rates:
                self.ui.combo_rate.setCurrentIndex(self.rates.index(int(defaults["samplerate"])))

        self.ui.prog_rssi.setMinimum(-60)
        self.ui.prog_rssi.setMaximum(0)

        if defaults["antenna"] is None:
            self.ui.combo_ant.setCurrentIndex(self.ui.combo_ant.findText("RX2"))
        else:
            self.ui.combo_ant.setCurrentIndex(self.ui.combo_ant.findText(defaults["antenna"]))

        #check KML by default, leave the rest unchecked.
        self.ui.check_sbs1.setChecked(bool(defaults["sbs1"] == "1"))
        self.ui.check_raw.setChecked(bool(defaults["raw"] == "1"))
        self.ui.check_fgfs.setChecked(bool(defaults["fgfs"] == "1"))
        self.ui.check_kml.setChecked(bool(defaults["kml"] == "1"))

        self.ui.line_sbs1port.insert(defaults["sbs1port"])#"30003")
        self.ui.line_rawport.insert(defaults["rawport"])#"9988")
        self.ui.line_fgfsport.insert(defaults["fgfsport"])#"5500")
        self.ui.line_kmlfilename.insert(defaults["kmlfile"])#"modes.kml")

        if defaults["latitude"] is not None:
            self.ui.line_my_lat.insert(defaults["latitude"])
        if defaults["longitude"] is not None:
            self.ui.line_my_lon.insert(defaults["longitude"])
        if defaults["apikey"] is not None:
            self.ui.line_my_api_key.insert(defaults["apikey"])
        
        #disable by default
        self.ui.check_adsbonly.setCheckState(QtCore.Qt.Unchecked)

        #set up the radio stuff
        self.queue = gr.msg_queue(10)
        self.running = False
        self.kmlgen = None #necessary bc we stop its thread in shutdown
        self.dbname = "air_modes.db"
        self.num_reports = 0
        self.last_report = 0
        self.context = zmq.Context(1)

        self.datamodel = dashboard_data_model(None)
        self.ui.list_aircraft.setModel(self.datamodel)
        self.ui.list_aircraft.setModelColumn(0)

        self.az_model = air_modes.az_map.az_map_model(None)
        self.ui.azimuth_map.setModel(self.az_model)

        #set up dashboard views
        self.icaodelegate = ICAOViewDelegate()
        self.ui.list_aircraft.setItemDelegate(self.icaodelegate)
        self.dashboard_mapper = QtGui.QDataWidgetMapper()
        self.dashboard_mapper.setModel(self.datamodel)
        self.dashboard_mapper.addMapping(self.ui.line_icao, 0)
        #self.dashboard_mapper.addMapping(self.ui.prog_rssi, 2)
        self.dashboard_mapper.addMapping(self.ui.line_latitude, 3)
        self.dashboard_mapper.addMapping(self.ui.line_longitude, 4)
        self.dashboard_mapper.addMapping(self.ui.line_alt, 5)
        self.dashboard_mapper.addMapping(self.ui.line_speed, 6)
        #self.dashboard_mapper.addMapping(self.ui.compass_heading, 7)
        self.dashboard_mapper.addMapping(self.ui.line_climb, 8)
        self.dashboard_mapper.addMapping(self.ui.line_ident, 9)
        self.dashboard_mapper.addMapping(self.ui.line_type, 10)
        self.dashboard_mapper.addMapping(self.ui.line_range, 11)

        compass_palette = QtGui.QPalette()
        compass_palette.setColor(QtGui.QPalette.Foreground, QtCore.Qt.white)
        self.ui.compass_heading.setPalette(compass_palette)
        self.ui.compass_bearing.setPalette(compass_palette)
        #TODO: change the needle to an aircraft silhouette
        self.ui.compass_heading.setNeedle(Qwt.QwtDialSimpleNeedle(Qwt.QwtDialSimpleNeedle.Ray, False, QtCore.Qt.black))
        self.ui.compass_bearing.setNeedle(Qwt.QwtDialSimpleNeedle(Qwt.QwtDialSimpleNeedle.Ray, False, QtCore.Qt.black))

        #hook up the update signal
        self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.dashboard_mapper.setCurrentModelIndex)
        self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.update_heading_widget)
        self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.update_bearing_widget)
        self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.update_rssi_widget)
        self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.update_map_highlight)
        self.datamodel.dataChanged.connect(self.unmapped_widgets_dataChanged)

        #hook up parameter-changed signals so we can change gain, rate, etc. while running
        self.ui.combo_rate.currentIndexChanged['QString'].connect(self.update_sample_rate)
        self.ui.line_gain.editingFinished.connect(self.update_gain)
        self.ui.combo_source.currentIndexChanged['QString'].connect(self.populate_source_options)

        #hook up live data text box update signal
        self.live_data_changed_signal.connect(self.on_append_live_data)

        self._last_live_data_update = time.time()
        self._pending_msgstr = ""

        self.prefs = None

    def update_sample_rate(self, rate):
        if self.running:
            self._radio.set_rate(int(float(rate)*1e6))

    def update_gain(self):
        if self.running:
            self._radio.set_gain(float(self.ui.line_gain.text()))

############ widget update functions for non-mapped widgets ############
    def update_heading_widget(self, index):
        if index.model() is not None:
            heading = index.model().data(index.model().index(index.row(), self.datamodel._colnames.index("heading"))).toDouble()[0]
            self.ui.compass_heading.setValue(heading)

    def update_bearing_widget(self, index):
        if index.model() is not None:
            bearing = index.model().data(index.model().index(index.row(), self.datamodel._colnames.index("bearing"))).toDouble()[0]
            self.ui.compass_bearing.setValue(bearing)

    def unmapped_widgets_dataChanged(self, startIndex, endIndex):
        index = self.ui.list_aircraft.selectionModel().currentIndex()
        if index.row() in range(startIndex.row(), endIndex.row()+1): #the current aircraft was affected
            if self.datamodel._colnames.index("heading") in range(startIndex.column(), endIndex.column()+1):
                self.update_heading_widget(index)
            if self.datamodel._colnames.index("bearing") in range(startIndex.column(), endIndex.column()+1):
                self.update_bearing_widget(index)
            if self.datamodel._colnames.index("rssi") in range(startIndex.column(), endIndex.column()+1):
                self.update_rssi_widget(index)

    def update_rssi_widget(self, index):
        if index.model() is not None:
            rssi = index.model().data(index.model().index(index.row(), 2)).toDouble()[0]
            self.ui.prog_rssi.setValue(rssi)

    def increment_reportspersec(self, msg):
        self.num_reports += 1

    def update_reportspersec(self):
        dt = time.time() - self.last_report
        if dt >= 1.0:
            self.last_report = time.time()
            self.ui.line_reports.setText("%i" % self.num_reports)
            self.num_reports = 0

    def update_map_highlight(self, index):
        if index.model() is not None:
            icaostr = index.model().data(index.model().index(index.row(), self.datamodel._colnames.index("icao"))).toString()
            icao = int(str(icaostr), 16)
            self.jsonpgen.set_highlight(icao)

##################### dynamic option population ########################
    #goes and gets valid antenna, sample rate options from the device and grays out appropriate things
    def populate_source_options(self):
        sourceid = self.ui.combo_source.currentText()
        self.rates = []
        self.ratetext = []
        self.antennas = []

        if sourceid == "UHD":
            try:
                from gnuradio import uhd
                self.src = uhd.single_usrp_source("", uhd.io_type_t.COMPLEX_FLOAT32, 1)
                self.rates = [rate.start() for rate in self.src.get_samp_rates()
                              if (rate.start() % 2.e6) == 0 and rate >= 4e6]
                self.antennas = self.src.get_antennas()
                self.src = None #deconstruct UHD source for now
                self.ui.combo_ant.setEnabled(True)
                self.ui.combo_rate.setEnabled(True)
                self.ui.stack_source.setCurrentIndex(0)
            except:
                self.rates = []
                self.antennas = []
                self.ui.combo_ant.setEnabled(False)
                self.ui.combo_rate.setEnabled(False)
                self.ui.stack_source.setCurrentIndex(0)

        elif sourceid == "Osmocom":
            try:
                import osmosdr
                self.src = osmosdr.source("")
                self.rates = [rate.start() for rate in self.src.get_sample_rates()
                             if ((rate.start() % 2.e6) == 0)
                             or (rate.start() < 4.e6 and ((rate.start()%0.2e6) == 0))]
                self.antennas = ["RX"]
                self.src = None
                self.ui.combo_ant.setEnabled(False)
                self.ui.combo_rate.setEnabled(True)
                self.ui.stack_source.setCurrentIndex(0)
            except:
                self.rates = []
                self.antennas = []
                self.ui.combo_ant.setEnabled(False)
                self.ui.combo_rate.setEnabled(False)
                self.ui.stack_source.setCurrentIndex(0)

        elif sourceid == "File/UDP":
            self.rates = [2e6*i for i in range(2,13)]
            self.antennas = ["None"]
            self.ui.combo_ant.setEnabled(False)
            self.ui.combo_rate.setEnabled(True)
            self.ui.stack_source.setCurrentIndex(1)

        self.ui.combo_rate.clear()
        self.ratetext = ["%.3f" % (rate / 1.e6) for rate in self.rates]
        for rate, text in zip(self.rates, self.ratetext):
            self.ui.combo_rate.addItem(text, rate)

        self.ui.combo_ant.clear()
        self.ui.combo_ant.addItems(self.antennas)

        #set up recommended sample rate
        if len(self.rates) > 1:
            if max(self.rates) > 4.e6:
                recommended_rate = min(x for x in self.rates if x >= 4e6 and
                                      max(self.rates) % x == 0)
            else:
                recommended_rate = max(self.rates)
            if recommended_rate >= 8.e6:
                self.ui.check_pmf.setChecked(True)
            else:
                self.ui.check_pmf.setChecked(False)
            self.ui.combo_rate.setCurrentIndex(self.rates.index(recommended_rate))

################ action handlers ####################
    def on_combo_source_currentIndexChanged(self, index):
        self.populate_source_options()

    def on_button_start_released(self):
        #if we're already running, kill it!
        if self.running is True:
            self.on_quit()

            self.num_reports = 0
            self.ui.line_reports.setText("0")

            self.ui.button_start.setText("Start")
            self.running = False

        else: #we aren't already running, let's get this party started
            parser = OptionParser(option_class=eng_option)
            air_modes.modes_radio.add_radio_options(parser)
            (options, args) = parser.parse_args() #sets defaults nicely
            if str(self.ui.combo_source.currentText()) != "File/UDP":
                options.source = str(self.ui.combo_source.currentText()).lower()
            else:
                options.source = str(self.ui.line_inputfile.text())
            options.rate = float(self.ui.combo_rate.currentText()) * 1e6
            options.antenna = str(self.ui.combo_ant.currentText())
            options.gain = float(self.ui.line_gain.text())
            options.threshold = float(self.ui.line_threshold.text())
            options.pmf = self.ui.check_pmf.isChecked()
            options.dcblock = self.ui.check_dcblock.isChecked()

            self._servers = ["inproc://modes-radio-pub"] #TODO ADD REMOTES
            self._relay = air_modes.zmq_pubsub_iface(self.context, subaddr=self._servers, pubaddr=None)

            if self.ui.check_raw.checkState():
                options.tcp = int(self.ui.line_rawport.text())

            self._radio = air_modes.modes_radio(options, self.context)
            self._publisher = pubsub()
            self._relay.subscribe("dl_data", air_modes.make_parser(self._publisher))

            try:
                my_position = [float(self.ui.line_my_lat.text()), float(self.ui.line_my_lon.text())]
            except:
                my_position = None

            try:
                my_apikey = str(self.ui.line_my_api_key.text())
            except:
                my_apikey = None

            self._cpr_dec = air_modes.cpr_decoder(my_position)

            self.datamodelout = dashboard_output(self._cpr_dec, self.datamodel, self._publisher)

            self.lock = threading.Lock() #grab a lock to ensure sql and kml don't step on each other

            #output options to populate outputs, updates
            if self.ui.check_kml.checkState():
                #we spawn a thread to run every 30 seconds (or whatever) to generate KML
                self.kmlgen = air_modes.output_kml(self.ui.line_kmlfilename.text(), self.dbname, my_position, self.lock) #create a KML generating thread

            if self.ui.check_sbs1.checkState():
                sbs1port = int(self.ui.line_sbs1port.text())
                sbs1out = air_modes.output_sbs1(self._cpr_dec, sbs1port, self._publisher)

            if self.ui.check_fgfs.checkState():
                fghost = "127.0.0.1" #TODO FIXME
                fgport = self.ui.line_fgfsport.text()
                fgout = air_modes.output_flightgear(self._cpr_dec, fghost, int(fgport), self._publisher)

            #add azimuth map output and hook it up
            if my_position is not None:
                self.az_map_output = air_modes.az_map.az_map_output(self._cpr_dec, self.az_model, self._publisher)
                #self._relay.subscribe("dl_data", self.az_map_output.output)

            #set up map
            #NOTE this is busted on windows. WebKit requires .htm[l] extensions to render,
            #so using a temp file doesn't work.
            self._htmlfile = open("/tmp/mode_s.html", 'wb+')#tempfile.NamedTemporaryFile()
            self._jsonfile = tempfile.NamedTemporaryFile()

            self.livedata = air_modes.output_print(self._cpr_dec,
                                                   self._publisher,
                                                   self.live_data_changed_signal.emit)

            #create SQL database for KML and dashboard displays
            self.dbwriter = air_modes.output_sql(self._cpr_dec, self.dbname, self.lock, self._publisher)
            self.jsonpgen = air_modes.output_jsonp(self._jsonfile.name, self.dbname, my_position, self.lock, timeout=1)
            htmlstring = air_modes.html_template(my_apikey, my_position, self._jsonfile.name)
            self._htmlfile.write(htmlstring)
            self._htmlfile.flush()
            class WebPage(QtWebKit.QWebPage):
                def javaScriptConsoleMessage(self, msg, line, source):
                    print('%s line %d: %s' % (source, line, msg))
            page = WebPage()
            self.ui.mapView.setPage(page)
            self.ui.mapView.load( QtCore.QUrl( QtCore.QUrl.fromLocalFile("/tmp/mode_s.html") ) )
            self.ui.mapView.show()

            #output to update reports/sec widget
            self._relay.subscribe("dl_data", self.increment_reportspersec)
            self._rps_timer = QtCore.QTimer()
            self._rps_timer.timeout.connect(self.update_reportspersec)
            self._rps_timer.start(1000)

            #start the flowgraph
            self._radio.start()

            self.ui.button_start.setText("Stop")
            self.running = True

            #grab prefs and save them
            self.prefs = {}
            self.prefs["samplerate"] = options.rate
            self.prefs["antenna"] = options.antenna
            self.prefs["gain"] = options.gain
            self.prefs["pmf"] = "1" if options.pmf else "0"
            self.prefs["dcblock"] = "1" if options.dcblock else "0"
            self.prefs["source"] = self.ui.combo_source.currentText()
            self.prefs["threshold"] = options.threshold
            self.prefs["sbs1"] = "1" if self.ui.check_sbs1.isChecked() else "0"
            self.prefs["sbs1port"] = int(self.ui.line_sbs1port.text())
            self.prefs["fgfs"] = "1" if self.ui.check_fgfs.isChecked() else "0"
            self.prefs["fgfsport"] = int(self.ui.line_fgfsport.text())
            self.prefs["raw"] = "1" if self.ui.check_raw.isChecked() else "0"
            self.prefs["rawport"] = int(self.ui.line_rawport.text())
            self.prefs["kml"] = "1" if self.ui.check_kml.isChecked() else "0"
            self.prefs["kmlfile"] = self.ui.line_kmlfilename.text()
            try:
                self.prefs["latitude"] = float(self.ui.line_my_lat.text())
                self.prefs["longitude"] = float(self.ui.line_my_lon.text())
            except:
                pass
            try:
                self.prefs["apikey"] = self.ui.line_my_api_key.text()
            except:
                pass
                
    def on_quit(self):
        if self.running is True:
            self._radio.close()
            self._radio = None
            self._relay.close()
            self._relay = None
            self._rps_timer = None
            try:
                self.kmlgen.done = True
                #TODO FIXME need a way to kill kmlgen safely without delay
                #self.kmlgen.join()
                #self.kmlgen = None
            except:
                pass

        if self.prefs is not None:
            self.write_defaults(self.prefs)

    #slot to catch signal emitted by output_live_data (necessary for
    #thread safety since output_live_data is called by another thread)
    def on_append_live_data(self, msgstr):
        self._pending_msgstr += msgstr + "\n"
        if time.time() - self._last_live_data_update >= 0.1:
            self._last_live_data_update = time.time()
            self.update_live_data(self._pending_msgstr)
            self._pending_msgstr = ""

    def update_live_data(self, msgstr):
        #limit scrollback buffer size -- is there a faster way?
        if(self.ui.text_livedata.document().lineCount() > 500):
            cursor = self.ui.text_livedata.textCursor()
            cursor.movePosition(QtGui.QTextCursor.Start)
            cursor.select(QtGui.QTextCursor.LineUnderCursor)
            cursor.removeSelectedText()

        self.ui.text_livedata.append(msgstr)
        self.ui.text_livedata.verticalScrollBar().setSliderPosition(self.ui.text_livedata.verticalScrollBar().maximum())

    opt_file = "~/.gr-air-modes/prefs"
    def get_defaults(self):
        defaults = {}
        defaults["samplerate"] = None #let app pick it
        defaults["pmf"] = None
        defaults["dcblock"] = None
        defaults["antenna"] = None
        defaults["gain"] = "25"
        defaults["kml"] = "1"
        defaults["kmlfile"] = "modes.kml"
        defaults["sbs1"] = "0"
        defaults["sbs1port"] = "30003"
        defaults["raw"] = "0"
        defaults["rawport"] = "9988"
        defaults["fgfs"] = "0"
        defaults["fgfsport"] = "5500"
        defaults["source"] = "UHD"
        defaults["threshold"] = "5"
        defaults["latitude"] = None
        defaults["longitude"] = None
        defaults["apikey"] = None

        prefs = ConfigParser.ConfigParser(defaults)
        prefs.optionxform = str

        try:
            prefs.read(os.path.expanduser(self.opt_file))
            for item in prefs.items("GUI"):
                defaults[item[0]] = item[1]
        except (IOError, ConfigParser.NoSectionError):
            print("No preferences file %s found, creating..." % os.path.expanduser(self.opt_file))
            self.write_defaults(defaults)

        return defaults


    def write_defaults(self, defaults):
        config = ConfigParser.RawConfigParser()
        config.add_section('GUI')

        for item in defaults:
            config.set('GUI', item, str(defaults[item]))

        dirname = os.path.dirname(os.path.expanduser(self.opt_file))
        if not os.path.exists(dirname):
            os.makedirs(dirname)

        with open(os.path.expanduser(self.opt_file), 'wb') as prefsfile:
            config.write(prefsfile)


if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    window = mainwindow()
    app.lastWindowClosed.connect(window.on_quit)
    window.setWindowTitle("Mode S/ADS-B receiver")
    window.show()
    sys.exit(app.exec_())

